/*=========================================================================
  FileName : Redir.aspx.cs


  This file is part of the Microsoft Commerce Server SDK

  Copyright (C) Microsoft Corporation.  All rights reserved.

  This source code is intended only as a supplement to Microsoft 
  Commerce Server and/or on-line documentation. See these other
  materials for detailed information regarding Microsoft code samples.

  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  PURPOSE.
=========================================================================*/
using System;
using System.Data;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using Microsoft.CommerceServer.Interop.Caching;
using Microsoft.CommerceServer.Runtime;
using Microsoft.CommerceServer.Runtime.Caching;
using Microsoft.CommerceServer.Runtime.Pipelines;

/// <summary>
/// This page handles redirects that result from Clicking on Displayable Campaign Items (Advertisements and Discounts)
/// that are displayed on the site. 
/// Requests to this page are handled by first calling the 'RecordEvent' Pipeline to record the fact that the Campaign Item
/// was clicked, and then redirecting the Request to the Url specified in the Campaign Item.
/// Note that if the page is accessed outside of the CSF context, or if the necessary caches and pipelines required for 
/// the page to execute are not set up, this page will internally generate an errors collection describing the reasons for
/// failure to run the RecordEvents pipeline. Currently, these errors are logged using Debug.WriteLine and Trace.Warn. 
/// Website Administrators may want to track these errors differently, or in more detail as deemed appropriate. 
/// A high volume of errors on this page could indicate badly authored campaign items, or potential attacks on the site.
/// </summary>
public partial class CSFRedirectPage : System.Web.UI.Page
{
	#region internal message strings

	/// <summary>
	/// These messages are hard-wired into this code file because they are currently only being used for tracing purposes.
	/// If these messages are going to be thrown as exceptions or displayed to the end user, then consider moving them to 
	/// a resource file in the web application and use a Resource Manager to retrieve them.
	/// </summary>
	private abstract class Messages
	{
		public const string CommerceContextNull = "CommerceContext.Current is null.";
		public const string PipelineNotFound = "A pipeline named '{0}' was not found in CommerceContext.Current.Pipelines.";
		public const string CacheNotFound = "A cache named '{0}' was not found in CommerceContext.Current.Caches.";
		public const string QueryStringValidationError = "The key '{0}' was either not found, or was invalid in the redirect page query string. The query string is below:\n{1}";
	}

	#endregion internal message strings

	/// <summary>
	/// This class handles the CSF redirect logic by parsing the query string. The RecordEvent method on this class
	/// records the information from the query string into the Marketing System. If the RecordEvent fails, it 
	/// returns false. The ErrorsCollection can be used to get more details about the errors that occured.
	/// </summary>
	private class CSFRedirectHandler
	{
		#region constants

		private const string QueryStringKeyCacheName = "cachename";
		private const string QueryStringKeyCampaignItemId = "ciid";
		private const string QueryStringKeyEventType = "evt";
		private const string QueryStringKeyPageGroupId = "PageGroupId";
		private const string QueryStringKeyRedirectUrl = "url";
		private const string DefaultEventType = "CLICK";

		#endregion constants

		#region fields

		int campaignItemId;
		int pageGroupId;
		string eventType;
		string cacheName;
		Uri redirectUrl;
		private List<string> errorsCollection;

		#endregion fields

		#region properties

		private int CampaignItemId
		{
			get 
			{ 
				return this.campaignItemId; 
			}
		}

		private int PageGroupId
		{
			get
			{ 
				return this.pageGroupId; 
			}
		}

		private string EventType
		{
			get 
			{ 
				return this.eventType; 
			}
		}

		private string CacheName
		{
			get 
			{ 
				return this.cacheName; 
			}
		}

		private bool IsValidRedirectQuery
		{
			get
			{
				return (this.errorsCollection.Count == 0);
			}
		}

		#endregion properties

		#region constructor
	
		/// <summary>
		/// Constructs an instance of the RedirectHandler using the query string.
		/// </summary>
		/// <param name="queryString"></param>
		public CSFRedirectHandler(System.Collections.Specialized.NameValueCollection queryString)
		{
			this.errorsCollection = new List<string>();

			this.campaignItemId = GetMarketingIdFromQueryString(queryString, QueryStringKeyCampaignItemId);
			
			this.pageGroupId = GetMarketingIdFromQueryString(queryString, QueryStringKeyPageGroupId);
			
			this.cacheName = queryString[QueryStringKeyCacheName];
			if(string.IsNullOrEmpty(this.cacheName))
			{
				LogInvalidKeyInQueryString(QueryStringKeyCacheName, queryString);
			}
			
			//optional event type - by default we set it to click
			this.eventType = queryString[QueryStringKeyEventType];
			if(string.IsNullOrEmpty(this.eventType))
			{
				Debug.WriteLine("No Event Type specified in Redirect Query. Assuming 'CLICK' event");
				this.eventType = DefaultEventType;
			}

			//optional redirect uri, by default we will just not do a redirect
			string redirectUrl = queryString[QueryStringKeyRedirectUrl];
			Uri.TryCreate(redirectUrl, UriKind.RelativeOrAbsolute, out this.redirectUrl);
			if (this.redirectUrl == null)
			{
				LogInvalidKeyInQueryString(QueryStringKeyRedirectUrl, queryString);
			}
		}

		#endregion constructor

		#region public methods

		/// <summary>
		/// Returns the Redirect Url that was specified in the query string.
		/// </summary>
		public Uri RedirectUrl
		{
			get
			{
				return this.redirectUrl;
			}
		}

		/// <summary>
		/// A collection of errors that occured during processing of the query string and the RecordEvent pipeline 
		/// (if any), or an empty collection.
		/// </summary>
		public IList<string> ErrorsCollection
		{
			get
			{
				return errorsCollection.AsReadOnly();
			}
		}

		/// <summary>
		/// Executes the record event pipeline. 
		/// </summary>
		/// <returns>Returns true if successful. False if there were errors.</returns>
		public bool RecordEvent()
		{
			const string RecordEventPipelineName = "recordevent";
			const string DictKeyWinners = "_winners";
			const string DictKeyEvent = "_event";
			const string DictKeyPerformance = "_performance";
			const string DictKeyFactory = "factory";
			const string DictKeySiteName = "SiteName";
			const string DictKeyPageGroupId = "PageGroupId";
			const string DictKeyContentList = "_content";
			const string DictKeyCache = "_cache";

			if(!this.IsValidRedirectQuery)
			{
				return false;
			}

			if (CommerceContext.Current == null)
			{
				this.errorsCollection.Add(Messages.CommerceContextNull);
				return false;
			}

			PipelineBase recordEventPipeline = CommerceContext.Current.Pipelines[RecordEventPipelineName];
			if (recordEventPipeline == null)
			{
				this.errorsCollection.Add(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Messages.PipelineNotFound, RecordEventPipelineName));
				return false;
			}

			CommerceCache csfCache = CommerceContext.Current.Caches[this.CacheName];
			if (csfCache == null)
			{
				this.errorsCollection.Add(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Messages.CacheNotFound, this.CacheName));
				return false;
			}

			IDictionary contextDictionary = null;
			IDictionary orderDictionary = null;
			IContentListFactory contentListFactory = null;
			IContentList contentList = null;
			try
			{
				//create context dictionary and set up keys 
				contextDictionary = new DictionaryClass();
				contextDictionary[DictKeySiteName] = CommerceContext.Current.SiteName;
				contextDictionary[DictKeyPageGroupId] = this.PageGroupId;

				//get the cache dictionary
				//we should not release this dictionary because it is referenced
				//by CommerceContext.Current. It will be released when 
				//CommerceContext.Current is disposed
				IDictionary csfCacheDictionary = (IDictionary)csfCache.GetCache();
				contextDictionary[DictKeyCache] = csfCacheDictionary;

				//create the order dictionary and set up keys
				orderDictionary = new DictionaryClass();
				orderDictionary[DictKeyWinners] = this.CampaignItemId;
				orderDictionary[DictKeyEvent] = this.EventType;
				orderDictionary[DictKeyPerformance] = csfCacheDictionary[DictKeyPerformance];
				contentListFactory = (IContentListFactory)csfCacheDictionary[DictKeyFactory];
				contentList = contentListFactory.CreateNewContentList();
				orderDictionary[DictKeyContentList] = contentList;
				recordEventPipeline.Execute(orderDictionary, contextDictionary);
				return true;
			}
			finally
			{
				if (contextDictionary != null)
				{
					Marshal.ReleaseComObject(contextDictionary);
				}
				if (orderDictionary != null)
				{
					Marshal.ReleaseComObject(orderDictionary);
				}
				if (contentListFactory != null)
				{
					Marshal.ReleaseComObject(contentListFactory);
				}
				if (contentList != null)
				{
					Marshal.ReleaseComObject(contentList);
				}
			}
		}

		#endregion public methods

		#region helper methods

		private void LogInvalidKeyInQueryString(string keyName,
		System.Collections.Specialized.NameValueCollection queryString)
		{
			string queryStringDisplay;
			if (queryString == null || queryString.Count == 0)
			{
				queryStringDisplay = "<null>";
			}
			else
			{
				queryStringDisplay = queryString.ToString();
			}
			this.errorsCollection.Add(string.Format(System.Globalization.CultureInfo.CurrentCulture,
				Messages.QueryStringValidationError, keyName, queryStringDisplay));
		}

		private int GetMarketingIdFromQueryString(System.Collections.Specialized.NameValueCollection queryString, 
			string keyName)
		{
			int tempResult = -1;
			string value = queryString[keyName];
			if (!string.IsNullOrEmpty(value))
			{
				Int32.TryParse(value, out tempResult);
			}
			if (tempResult < 0)
			{
				LogInvalidKeyInQueryString(keyName, queryString);
			}
			return tempResult;
		}

		#endregion helper methods 
	}

    protected void Page_Load(object sender, EventArgs e)
    {
		CSFRedirectHandler redirectHandler = new CSFRedirectHandler(Request.QueryString);

		if (!redirectHandler.RecordEvent())
		{
			//Sites should do more detailed error handling and logging
			foreach (string error in redirectHandler.ErrorsCollection)
			{
				Debug.WriteLine(error);
				Trace.Warn(error);
			}
		}
		//finally, lets try to redirect the user if a valid redirect url was specified
		if (redirectHandler.RedirectUrl != null)
		{
			Response.Redirect(redirectHandler.RedirectUrl.OriginalString);
		}
    }
}
